package edu.lambdateacher.parser;

/*
LambdaTeacher
Copyright (C) 2000  Kim Mason
Copyright (C) 2014  Vitalij Zadneprovskij

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
*/

import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;

import edu.lambdateacher.PropertiesManager;

/**
 * This class takes in a Stream, and returns a List of tokens. It is used by
 * the LambdaScanner to tokenise the input stream.
 */
public class StreamTokeniser {
	
	private final static Logger LOGGER = Logger.getLogger(StreamTokeniser.class);
	
	private Reader inputReader;

	// these are used to convert the Stream into a list of Tokens
	private char currentCharacter;
	private int currentPosition;
	private int lineNumber;
	private List<LambdaToken> tokenList;
	private boolean endOfFile;

	public synchronized List<LambdaToken> tokeniseStream(Reader inread) {
		
		if(inread == null){
			throw new IllegalArgumentException("The stream to tokenise cannot be null");
		}
		
		inputReader = inread;

		currentPosition = 1;
		lineNumber = 1;
		currentCharacter = (char) 0;
		tokenList = new ArrayList<LambdaToken>();
		endOfFile = false;
		LambdaToken tok;

		if (inputReader == null) {
			((ArrayList)tokenList).trimToSize();
			return tokenList;
		}
		// Position currentchar at zeroth character, and curpos at zeroth
		// character.
		advance();
		do {
			tok = nextToken();
			LOGGER.debug(tok.print());
			tokenList.add(tok);
		} while (tok.getTokenType() != LambdaTokenType.EOF);

		((ArrayList)tokenList).trimToSize();
		return tokenList;
	}

	private void skipCommentAndWhitespace() {
		skipWhiteSpace();
		while (currentCharacter == '{') {
			// while we have a comment
			while (currentCharacter != '}') { // if we have a linefeed
				if (currentCharacter == '\n') {
					lineNumber = lineNumber + 1;
					currentPosition = 1;
					advance();
					// check for a CR
					if (currentCharacter == '\r') {
						currentPosition = 1;
						advance();
					}
				} else
				// if we have a carriage return
				if (currentCharacter == '\r') {
					lineNumber = lineNumber + 1;
					currentPosition = 1;
					advance();
					// check for a LF
					if (currentCharacter == '\n') {
						currentPosition = 1;
						advance();
					}
				} else
				// twas just comment
				{
					advance();
				}
			}
			// now step over the '}'
			advance();
			skipWhiteSpace();
		}

	}

	private void skipWhiteSpace() {
		// While we haven't reached eof then skip white space (tab, LF, CR,
		// space, <CR, LF>, and
		// <LF, CR>)
		while (Character.isWhitespace(currentCharacter)) {
			// if we have a linefeed
			if (currentCharacter == '\n') {
				lineNumber = lineNumber + 1;
				currentPosition = 1;
				advance();
				// check for a CR
				if (currentCharacter == '\r') {
					currentPosition = 1;
					advance();
				}
			} else
			// if we have a carriage return
			if (currentCharacter == '\r') {
				lineNumber = lineNumber + 1;
				currentPosition = 1;
				advance();
				// check for a LF
				if (currentCharacter == '\n') {
					currentPosition = 1;
					advance();
				}
			} else
			// twas just white space, so advance
			{
				advance();
			}
		}
	}

	private LambdaToken createIdentifierToken(){
		LambdaToken lambdaToken = null;
		
		// Parse an identifier
		String identifier = "";

		do {
			identifier = identifier + currentCharacter;
			advance();
		} while ( Character.isAlphabetic(currentCharacter) || Character.isDigit(currentCharacter) );

		// If you had reserved words, you'd check for them here
		if (identifier.equalsIgnoreCase("LET")) {
			lambdaToken = new LambdaToken(LambdaTokenType.LET);
		} else {
			lambdaToken = new LambdaToken(LambdaTokenType.IDENT, identifier);
		}
		
		return lambdaToken;
	}
	
	private LambdaToken createNumberToken(){
		LambdaToken lambdaToken = null;
		
		StringBuffer buffer = new StringBuffer();

		while (Character.isDigit(currentCharacter)) {
			buffer.append(currentCharacter);
			advance();
		}
		try {
			int tempint = Integer.parseInt(buffer.toString());
			lambdaToken = new LambdaToken(LambdaTokenType.NUMBER, tempint);
		} catch (NumberFormatException e1) {
			LOGGER.error("Error - Invalid number at position: " + Integer.toString(currentPosition),e1);
		}
		
		return lambdaToken;
	}
	
	/**
	 * We have a standard token (Lambda, LeftParen, RightParen, Period, 
	 * plus, minus, star, slash), or an eof (end of string)
	 * @return
	 */
	private LambdaToken createStandardToken(){


		LambdaToken lambdaToken = null;
		
		Map<Character,LambdaTokenType> charMap = LambdaTokenType.getCharMap();
		char currentlyAnalyzedChar = currentCharacter;
		if(LambdaTokenType.getCharMap().containsKey(currentlyAnalyzedChar)){
			LambdaTokenType tokenType;
			if( ! endOfFile ){
				advance();
			}
			tokenType = charMap.get(currentlyAnalyzedChar);
			lambdaToken = new LambdaToken(tokenType);
		}else{
			advance();
			
			if(endOfFile){
				lambdaToken = new LambdaToken(LambdaTokenType.EOF);
			}else{			
				lambdaToken = new LambdaToken(LambdaTokenType.UNKNOWN);
			}
		}
		
		return lambdaToken;
	}
	
	/**
	 * Returns the next lambda token that is in the stream 
	 * @return the next lambda token that is in the stream 
	 */
	private LambdaToken nextToken() {
		LambdaToken lambdaToken = null;
		int startpos;

		// set the position at which the token starts
		startpos = currentPosition;

		// skip over any whitespace and comments
		skipCommentAndWhitespace();

		// We have an identifier
		
		if (Character.isAlphabetic(currentCharacter) && currentCharacter != PropertiesManager.LAMBDA_CHAR) {
			lambdaToken = createIdentifierToken();
		} else if (Character.isDigit(currentCharacter)) { // parse a number
			lambdaToken = createNumberToken();
		} else {
			lambdaToken = createStandardToken();
		}
		lambdaToken.setLineAndPosition(lineNumber, startpos);
		return lambdaToken;
	}

	/**
	 * advance returns the next non whitespace character, or -1 if eof (the end
	 * of the string) has been reached.
	 */
	private void advance() {
		try {
			currentCharacter = (char) inputReader.read();
			
			if(currentCharacter == (char) -1){
				endOfFile = true;
			}
		} catch (IOException e1) {
			LOGGER.error("Error - Scanning error - invalid input length. This is a bug!",e1);
		}
	}

	@Override
	public String toString() {
		return "StreamTokeniser [theinread=" + inputReader + ", currentchar=" + currentCharacter + ", curpos=" + currentPosition + ", line_no=" + lineNumber + ", tokenvector=" + tokenList + "]";
	}
}
